package org.melanesia.beans;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.melanesia.beans.exceptions.NoGetterFoundException;
import org.melanesia.beans.exceptions.NoSetterFoundException;
import org.melanesia.conf.Config;

/**
 * A factory class, for creating property accessors.
 *
 * @author marcin.kielar
 */
public abstract class AccessorFactory {

    /**
     * Property accessor cache - an LRU list for property accessors. This list
     * is populated inside {@link #createAccessor(Class, String)} when the
     * accessor is first created.
     *
     * This is an optimization hack. Thanks to this list, less reflection is
     * used, and the overal process of mapping result set values to bean
     * properties is much faster, especially with large result sets, where the
     * mapping uses the same accessors for every row.
     */
    private static final Map<String, PropertyAccessor> PROPERTY_ACCESSOR_CACHE = new LinkedHashMap<String, PropertyAccessor>(
            Config.getReflectionDataCacheSize(), 0.75F, true) {

        /** Serial version uid for serialization. */
        private static final long serialVersionUID = -7569227739343284480L;

        @Override
        public boolean removeEldestEntry(final Map.Entry<String, PropertyAccessor> eldest) {
            return size() > Config.getReflectionDataCacheSize();
        }
    };

    /**
     * Creates property accessor for a given property name in given class.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return property accessor
     */
    public static final PropertyAccessor createAccessor(final Class<?> beanClass, final String propertyName) {

        String cacheKey = beanClass.getName() + "***" + propertyName;
        PropertyAccessor propertyAccessor = PROPERTY_ACCESSOR_CACHE.get(cacheKey);

        if (propertyAccessor == null) {

        	String javaPropertyName = BeanUtils.strip(propertyName, "?");
            Setter setter = findSetter(beanClass, javaPropertyName);
            Getter getter = findGetter(beanClass, javaPropertyName);

            if (setter == null) {
                throw new NoSetterFoundException("Could not create accessor for property \"" + propertyName + "\" on class \"" + beanClass
                        + "\".");
            }

            if (getter == null) {
                throw new NoGetterFoundException("Could not create accessor for property \"" + propertyName + "\" on class \"" + beanClass
                        + "\".");
            }

            propertyAccessor = new PropertyAccessor(propertyName, setter, getter);
            PROPERTY_ACCESSOR_CACHE.put(cacheKey, propertyAccessor);
        }

        return propertyAccessor;
    }

    /**
     * Finds setter for given property name. The order of searching: - For
     * simple property: first method setter, then field setter. - For composite
     * property: first method setter, then field setter for each of the simple
     * properties in path. In this case a getter is also required, to check id
     * intermediate property is or is not null. The algorithm for getters is the
     * same.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return setter
     */
    private static Setter findSetter(final Class<?> beanClass, final String propertyName) {

        Setter setter = null;

        // Check special types first
        if (BeanUtils.isAssignableFrom(Map.class, beanClass)) {
            setter = new MapSetter(propertyName);
        } else {

            if (BeanUtils.isSimpleProperty(propertyName)) {

                // Simple property, just find setter
                setter = findMethodSetter(beanClass, propertyName);
                if (setter == null) {
                    setter = findFieldSetter(beanClass, propertyName);
                }
                if (setter == null) {
                    setter = findCollectionGetSetter(beanClass, propertyName);
                }

            } else {

                // Composite property, this is harder
                setter = findCompositeSetter(beanClass, propertyName);
            }
        }

        return setter;
    }

    /**
     * Finds setter for a collection property where there is no explicit setter,
     * just a {@link Collection} {@code getXXX()} method.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return getter
     */
    private static Setter findCollectionGetSetter(final Class<?> beanClass, final String propertyName) {

        Setter setter = null;

        Getter getter = findGetter(beanClass, propertyName);
        if (getter != null && BeanUtils.isAssignableFrom(Collection.class, getter.getReturnType())) {
            setter = new CollectionGetSetter(getter);
        }

        return setter;
    }

    /**
     * Finds getter for given property name. The order of searching: - For
     * simple property: first method getter, then field getter. - For composite
     * property: first method getter, then field getter for each of the simple
     * properties in path In this case a setter is also required, in order to
     * instantiate intermediate properties. The algorithm for setters is the
     * same.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return getter
     */
    private static Getter findGetter(final Class<?> beanClass, final String propertyName) {
        Getter getter = null;

        // Check special types first
        if (BeanUtils.isAssignableFrom(Map.class, beanClass)) {
            getter = new MapGetter(propertyName);
        } else {
            if (BeanUtils.isSimpleProperty(propertyName)) {
                // Simple property
                getter = findMethodGetter(beanClass, propertyName);

                if (getter == null) {
                    getter = findFieldGetter(beanClass, propertyName);
                }
            } else {
                // Composite property
                getter = findCompositeGetter(beanClass, propertyName);
            }
        }

        return getter;
    }

    /**
     * Finds field getter for a simple property.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return field getter
     */
    private static FieldGetter findFieldGetter(final Class<?> beanClass, final String propertyName) {

        String setterFieldName = propertyName;

        for (Field field : beanClass.getFields()) {

            if (setterFieldName.equalsIgnoreCase(field.getName())) {
                return new FieldGetter(field);
            }
        }

        return null;
    }

    /**
     * Finds field setter for a simple property.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return field setter
     */
    private static FieldSetter findFieldSetter(final Class<?> beanClass, final String propertyName) {

        String setterFieldName = propertyName;

        for (Field field : beanClass.getFields()) {
            if (setterFieldName.equalsIgnoreCase(field.getName())) {
                return new FieldSetter(field);
            }
        }

        return null;
    }

    /**
     * Finds method getter for a simple property. The property name is
     * "normalized" using {@link BeanUtils#sqlToCamelCase(String)} and
     * {@link BeanUtils#capitalize(String)}. After normalization, prefix "get"
     * and "is" are added to the normalized property creating method name.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return method getter
     */
    private static MethodGetter findMethodGetter(final Class<?> beanClass, final String propertyName) {

        String getterMethodName1 = "get" + BeanUtils.capitalize(propertyName);
        String getterMethodName2 = "is" + BeanUtils.capitalize(propertyName);

        for (Method method : beanClass.getMethods()) {

            if (getterMethodName1.equalsIgnoreCase(method.getName()) || getterMethodName2.equalsIgnoreCase(method.getName())) {
                return new MethodGetter(method);
            }
        }

        return null;
    }

    /**
     * Finds method setter for a simple property. The property name is
     * "normalized" using {@link BeanUtils#sqlToCamelCase(String)} and
     * {@link BeanUtils#capitalize(String)}. After normalization, prefix "set"
     * is added to the normalized property creating method name.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            property name
     * @return method setter
     */
    private static MethodSetter findMethodSetter(final Class<?> beanClass, final String propertyName) {

        String setterMethodName = "set" + BeanUtils.capitalize(propertyName);

        for (Method method : beanClass.getMethods()) {

            if (setterMethodName.equalsIgnoreCase(method.getName())) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes != null && parameterTypes.length == 1) {
                    return new MethodSetter(method);
                }
            }
        }

        return null;
    }

    /**
     * Finds a composite getter for a composite property.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            composite property name
     * @return composite getter
     */
    private static CompositeGetter findCompositeGetter(final Class<?> beanClass, final String propertyName) {
        List<PropertyAccessor> list = new ArrayList<PropertyAccessor>();
        fillAccessorChain(list, beanClass, propertyName);

        return new CompositeGetter(list);
    }

    /**
     * Finds a composite setter for a composite property.
     *
     * @param beanClass
     *            bean class
     * @param propertyName
     *            composite property name
     * @return composite setter
     */
    private static CompositeSetter findCompositeSetter(final Class<?> beanClass, final String propertyName) {

        List<PropertyAccessor> list = new ArrayList<PropertyAccessor>();
        fillAccessorChain(list, beanClass, propertyName);

        return new CompositeSetter(list);
    }

    /**
     * Fills <code>accessorChain</code> list with property accessors for each of
     * the simple properties on a composite property path. Such list creates an
     * accessor chain, ie. list of accessors, that - when subsequently called -
     * accesses the least element on the composite property path, thus realising
     * the composite property accessor.
     *
     * @param accessorChain
     *            list of accessors to fill
     * @param beanClass
     *            bean class
     * @param propertyName
     *            composite property name
     */
    private static void fillAccessorChain(final List<PropertyAccessor> accessorChain, final Class<?> beanClass, final String propertyName) {

        if (BeanUtils.isSimpleProperty(propertyName)) {

            // finally last bean
            PropertyAccessor accessor = createAccessor(beanClass, propertyName);
            accessorChain.add(accessor);

        } else {

            String simplePropertyName = BeanUtils.getSimpleProperty(propertyName);
            String subPropertyName = BeanUtils.getSubProperty(propertyName);

            PropertyAccessor accessor = createAccessor(beanClass, simplePropertyName);
            accessorChain.add(accessor);

            // Final-recurrent call to create getters for rest of the beans
            fillAccessorChain(accessorChain, accessor.getPropertyClass(), subPropertyName);
        }
    }
}
